import Image
import os
import logging
import sys
from optparse import OptionParser
from ConfigParser import ConfigParser

handler = logging.StreamHandler(sys.stdout)
frm = logging.Formatter ("%(asctime)s %(levelname)s: %(message)s", "%H:%M:%S")
handler.setFormatter(frm)
logger = logging.getLogger()
logger.addHandler (handler)
logger.setLevel (logging.INFO)

(F_LANDSCAPE,F_PORTRAIT) = (0,1)

def parse_config ():
    cf = {}
    user_ini = os.path.expanduser('~/.printcheck/formats.ini')
    config = ConfigParser()

    if os.path.isfile(user_ini):
        config.read(user_ini)
    else:
        config.read('formats.ini')

    for section in config.sections():
        cf.update (
            {
            config.get(section,'alias'):
            (int(config.get(section,'width')),
            int(config.get(section,'height')),
            int(config.get(section,'dpi')),
            config.get(section,'unit'))
            }
        )
    return cf




class FormatStore (list):
    def __init__ (self):
        formats = parse_config()
        for alias, fdata in formats.items():
            self.append (Format (alias,fdata[0],fdata[1],fdata[2],fdata[3]))
        self.sort()

    def _match_format (self,photo_obj):
        target_fmt = None

        for fmt in self:
            printable = fmt.is_printable(photo_obj)
            if printable is F_LANDSCAPE:
                target_fmt = fmt.alias + ' (Landscape)'
            elif printable is F_PORTRAIT:
                target_fmt = fmt.alias + ' (Portrait)'
            else:
                target_fmt = None

            yield target_fmt

    def match_biggest_format (self,photo_obj):
        biggest_fmt = None
        for fmt in self._match_format(photo_obj):
            if fmt is not None:
                biggest_fmt = fmt

        return biggest_fmt

    def _return_format (self,alias):
        match_fmt = None
        restr = {'+p': F_PORTRAIT,'+l': F_LANDSCAPE}
        restr_to = None

        for fmt in self:
            alw = alias.lower()
            if alw.endswith('+l') or alw.endswith('+p'):
                restr_to = restr.get(alw[-2:])
                alw = alw[:-2]
            if fmt.alias.lower() == alw:
                match_fmt = fmt
                if restr_to is not None:
                    match_fmt.restricted = restr_to
                    logger.debug ('format restricted to %s', restr_to)
        return match_fmt

    def match_format_alias (self,photo_obj,alias):
        match_fmt = self._return_format(alias)
        if match_fmt is None:
            raise SyntaxError ("Error: Unknown Format: %s" % alias)

        printable = match_fmt.is_printable (photo_obj)

        if printable is not None:
            if printable == F_LANDSCAPE:
                return "Yes (Landscape)"
            if printable == F_PORTRAIT:
                return "Yes (Portrait)"

    def match_all_formats (self,photo_obj):
        all_fmts = []
        for fmt in self._match_format(photo_obj):
            if fmt is not None:
                all_fmts.append (fmt)

        return all_fmts


class Format (object):
    def __init__ (self, alias, width, height, resolution, unit='mm'):
        self.alias = alias
        self.width = width
        self.height = height
        self.unit = unit
        self.resolution = resolution
        self.restricted = None
        self.megapixels = self.get_megapixel()
        self.ratio_landscape = float(self.width) / self.height
        self.ratio_portrait = float(self.height) / self.width

    def __cmp__ (self, other):
        if self.megapixels > other.megapixels:
            return 1
        elif self.megapixels == other.megapixels:
            return 0
        else:
            return -1

    def get_megapixel (self):
        """calculates the size in megapixels for a certain format"""
        conv_to_inch = {'mm':   25.4,
                        'inch': 1,
                        'cm':   2.54,
                        'm':    0.0254,
                        'dm':   0.254
                        }
        conv_factor = conv_to_inch.get(self.unit.lower(),25.4)
        return float ((self.height*self.width/conv_factor**2)/ \
                       (1000*1000)*self.resolution**2)

    def is_printable (self, photo_obj, res_tolerance=0.1, rat_tolerance=0.1):
        """defines if a photo is printable on the current format
           photo_obj        a photo object

           res_tolerance
           resolution tolerance from 0.0 (0%) to 1.0 (100%)
           determines how much the photo resolution might be below the format
           resolution to be still be printed. A 10x15 cm printout requires a
           1.8 megapixel photo. When you set the tolerance to 0.2, a 1.5
           megapixel photo would also be accepted.

           ratio tolerance
           ratio tolerance from 0.0 (0%) to 1.0 (100%)
           determines how much the photo and format ratio may differ.
           This can be useful when you cropped out small parts of the photo
           which caused the ratio to change.
         """
        if photo_obj.megapixels * (1 + res_tolerance) >= self.megapixels:
            ph_rlb = photo_obj.ratio * (1 - rat_tolerance)
            ph_rub = photo_obj.ratio * (1 + rat_tolerance)

            if (ph_rlb <= self.ratio_landscape <= ph_rub) and self.restricted != F_PORTRAIT:
                logger.debug ("Identified as Landscape")
                return F_LANDSCAPE

            if ph_rlb <= self.ratio_portrait <= ph_rub and self.restricted != F_LANDSCAPE:
                logger.debug ("Identified as Portrait")
                return F_PORTRAIT

        logger.debug ('Image %s has just %s mpx, but %s mpx are required' % \
                     (photo_obj.photo_path, photo_obj.megapixels, self.megapixels))
        return None


class Photo (object):
    def __init__(self, photo_path):
        """opens photo_path and lets PIL read the image data
           tries to extract EXIF orientation data and changes
           width/height data accordingly
        """
        self.photo = Image.open (photo_path)
        logger.debug ('Sucessfully opened %s' % photo_path)
        self.photo_path = photo_path
        (self.width, self.height) = self.photo.size

        #Check for presence of EXIF data
        if hasattr (self.photo,'_getexif'):
            exiftags = self.photo._getexif()
            #fetch orientation tag, default = 1 (no rotation)
            orientation = exiftags.get (0x0112,1)
            #indication of a portrait mode - swap width and height
            if orientation > 4:
                logger.debug ('EXIF orientation tag says: photo shot in portrait mode')
                (self.height,self.width) = self.photo.size
        logger.debug ('width: %s, height: %s' % (self.width, self.height))
        self.ratio = float (self.width)/self.height
        logger.debug ('ratio %s:' % self.ratio)
        self.megapixels = float (self.width*self.height)/1000000
        logger.debug ('megapixels: %s' % self.megapixels)

    def match_format (self):
        formats = FormatStore()
        formats.match_size(self)


if __name__ == '__main__':
    list_formats = None
    parser = OptionParser(usage="ppc -m [lf|af|format] file/dir", version="ppc 0.1")
    parser.add_option("-m",'--match')
    parser.add_option("-l",'--list', action="store_true", dest="list_formats",\
                      help="list all predefined formats in formats.ini")
    #parser.add_option("-p",'--pt', help="megapixel tolerance [%]")
    #parser.add_option("-r",'--rt', help="resolution tolerance [%]")
    (options, args) = parser.parse_args()
    ph_files = []
    formats = FormatStore()
    if options.match is not None and len(args) == 1:
        fname = args[-1]
        if os.path.isfile(fname):
            try:
                ph_files.append(Photo(fname))
            except:
                raise IOError ('%s is in an invalid format' % fname)
        elif os.path.isdir (fname):
            for ph_file in os.listdir(fname):
                try:
                    ph_entry = Photo (os.path.join(fname,ph_file))
                    ph_files.append (ph_entry)
                except:
                    pass
        else:
            raise IOError ('%s is not a valid file or directory' % fname)

        for phf in ph_files:
            ph_name = os.path.split(phf.photo_path)[1]
            if options.match in ['lf', 'largestformat']:
                bf = formats.match_biggest_format(phf)
                print "%s \t %sx%spx\t %s" % (ph_name, phf.width,phf.height, bf)
            elif options.match in ['af','allformats']:
                af = formats.match_all_formats(phf)
                print "%s \nDimensions:\t%sx%s Pixels\nFormats:\t%s" % \
                (ph_name, phf.width,phf.height, '\n\t\t'.join(af))
            else:
                al = formats.match_format_alias(phf,options.match)
                print "%s (%sx%spx) is printable on %s:\t%s" % \
                (ph_name, phf.width, phf.height,options.match.upper(), al)

    elif options.list_formats is True:
        print "\tNAME \t DIMENSIONS\t DPI"
        for format in formats:
            print "\t%s \t %s x %s %s\t %s" % \
            (format.alias, format.width, format.height, format.unit, format.resolution)

    else:
        print parser.usage



